﻿using System;
using System.Text;
using System.Windows;
using System.Linq;
using System.ComponentModel;
using System.Globalization;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Collections;
using System.Collections.Generic;
using System.Data.Services.Client;
using System.Data.Services.Common;

using ex = System.Linq.Expressions;

// Author: Miroslav Matula m.matula@alutam.cz 
// Date : 9.9.2010

namespace Slma.OData
{
  /// <summary>
  /// 
  /// </summary>
  public class ODataSourceView :DependencyObject,  IEnumerable, INotifyPropertyChanged, INotifyCollectionChanged, IList
    , ICollectionView, IPagedCollectionView   ,IEditableCollectionView 
  {

    public enum enCurrentPosition { None, First, Last };

    #region Ctor
    public ODataSourceView(DataServiceContext dataServiceContext_, string ODataEntitySetName_)
    {
      _dataServiceContext = dataServiceContext_;
      _dataServiceContext.MergeOption = MergeOption.AppendOnly;

      _ODataEntitySetName = ODataEntitySetName_;
      _ODataEntitySetQuery = ODataEntitySetName_;

      var prInfo = dataServiceContext_.GetType().GetProperty(ODataEntitySetName_);
      if(prInfo == null || prInfo.PropertyType.BaseType.Name != "DataServiceQuery") throw new Exception("The EntitySetName " + ODataEntitySetName_ + " is invalid");
      //ODataEntitySetName = EntitySetName_;
      _ODataEntityType = prInfo.PropertyType.GetGenericArguments()[0];
      // Primary Key
      var keyAttr = _ODataEntityType.GetCustomAttributes(typeof(DataServiceKeyAttribute), false)[0] as DataServiceKeyAttribute;
      _primaryKeyNames = keyAttr.KeyNames;
      // Entity_gen -- 
      _EntityType = Slma.OData.Factory.ClassFactory.GetAnonymousType(_ODataEntityType);
      // v DesignMode nepoužívej Download
      this._allowLoadData = !System.ComponentModel.DesignerProperties.IsInDesignTool;  
      PageSize = 10;
      LoadSize = 30;
      this._supportedOperations = EntityListOperations.All;


      IncludeTotalCount = true;

      _currentPosition = -1;
      _startIndexBuffer = -1; // indikace, že chci nová data
      TotalItemCount = -1;
      EvalCanLoad();
      EvalCanChangePage();
    }


    #endregion

    #region -- Fields -------------------------
    private Action DataLoadCompleteInternal;
    public Action DataLoadComplete;

    // DataContext, přes toto dělám všechny datové operace
    private DataServiceContext _dataServiceContext;
    private string _ODataEntitySetName; // Název seznamu entit např. "Products"
    private string _ODataEntitySetQuery; // = _ODataEntitySetName
    private Type _ODataEntityType; // Typ Entity např. Product, Customer
    private Type _EntityType; // Generovaná Entita z OData.Entity
    private ReadOnlyCollection<string> _primaryKeyNames;
    internal int _suppressCollectionChangedCnt = 0; // >0 zakazuje generovat eventy. Slouží pro pro back operace
    private int _deferredLevel; // The number of requests to defer Refresh()

    private List<object> _dataView = new List<object>();
    private List<object> _dataBuffer = new List<object>();

    private EntityListOperations _supportedOperations;


    public bool _allowLoadData = true; //může lod data. Používám v případě testování -DesignMode
    private int _startIndexBuffer = -1; // pozice vůči začátku data
    private int _startIndexView = 0;  // pozice vůči začátku _dataBuffer

    private int _deferredPageIndex;// úschova data pro pro konec asynchr. čtení
    private bool _deferredDirectionUp;// úschova data pro pro konec asynchr. čtení
    private enCurrentPosition _deferredSetCurrenposition;// úschova data pro pro konec asynchr. čtení
    private bool _deferredNeedRefresh; // 

    //private object _editItem;

    #endregion

    #region Property


    // Type od class from OData proxy
    public Type ODataEntityType { get { return _ODataEntityType; } }
    //---- není DesignMode, nemá změny, není LoadingData, SubmittingChanges
    public bool CanLoad
    {
      get { return _canLoad; }
      private set
      {
        if(value == _canLoad) return;
        _canLoad = value;
        OnPropertyChanged("CanLoad");
      }
    }
    private bool _canLoad;
    private void EvalCanLoad() {CanLoad = _allowLoadData && !IsBusy && !HasChanges; }
    // true=každý dotaz na query vrací TotalCount. Pozor-zatěžuje výkonově server
    public bool IncludeTotalCount { get; set; } 
    //---- Má změny Add,Delete,Update - nelze přejít na další stránku ani udělat LoadData nebo Refresh Dat
    public bool HasChanges { 
      get { return _hasChanges; }
      set { 
        if(value == _hasChanges)return;
        _hasChanges = value;
        OnPropertyChanged("HasChanges");
        EvalCanLoad();
        EvalCanChangePage();
      }
    }
    private bool _hasChanges;
    private void EvalHasChanges()
    {
      bool has = false;
      foreach(Entity entity in this._dataBuffer) {
        if(entity.IsChanged) {
          has = true;
          break;
        }
      }
      HasChanges = has;
    }

    // Počkej se změnou stránky. Když je IsDeffered, neprováděj submitChanges, cancelChanges, cancelLoad  
    public bool IsDeferred { get { return _deferredLevel > 0; } }
    //--
    public bool IsBusy { get { return IsBusyCnt > 0; } }
    private int IsBusyCnt
    {
      get { return _isBusyCnt; }
      set
      {
        if(_isBusyCnt == value) return;
        var isbusy = _isBusyCnt > 0;
        _isBusyCnt = value;
        if(isbusy != IsBusy) {
          OnPropertyChanged("IsBusy");
        }
      }
    }
    private int _isBusyCnt;
    //--
    public bool IsLoading { get { return IsLoadingCnt > 0; } }
    private int IsLoadingCnt
    {
      get { return _isLoadingCnt; }
      set
      {
        if(_isLoadingCnt == value) return;
        var isLoading = _isLoadingCnt > 0;
        _isLoadingCnt = value;
        if(isLoading != IsLoading) {
          OnPropertyChanged("IsLoading");
        }
      }
    }
    private int _isLoadingCnt;
    //--
    public bool IsSubmitting { get { return IsSubmittingCnt > 0; } }
    private int IsSubmittingCnt
    {
      get { return _isSubmittingCnt; }
      set
      {
        if(_isSubmittingCnt == value) return;
        var isSubmitting = _isSubmittingCnt > 0;
        _isSubmittingCnt = value;
        if(isSubmitting != IsSubmitting) {
          OnPropertyChanged("IsSubmitting");
        }
      }
    }
    private int _isSubmittingCnt;
    //--
    public bool IsPageChanging
    {
      get { return _isPageChanging != 0; }
    }
    private int _isPageChanging;
    private int IsPageChangingInternal
    {
      get { return _isPageChanging; }
      set
      {
        var old = _isPageChanging != 0;
        if(_isPageChanging == value) return;
        _isPageChanging = value;
        if(old != (_isPageChanging != 0)) {
          OnPropertyChanged("IsPageChanging");
        }
      }
    }
    //--
    // Pokud neznám, pak -1
    private int LastPageIndex
    {
      get
      {
        if(_totalItemCount < 0) return -1; // nepodařilo se načíst počet řádků
        //výpočet
        int lastPageIndex = 0;
        if(_totalItemCount > 0) {
          lastPageIndex = _totalItemCount / _pageSize;
          if((_totalItemCount % _pageSize) == 0) lastPageIndex--;
        }
        return lastPageIndex;
      }
    }
    public int LoadSize { 
      get { return _loadSize; }
      set {
        if(value < 0) value = 0;
        if(_loadSize == value)return;
        _loadSize = value;
        OnPropertyChanged("LoadSize");
      } 
    }
    private int _loadSize;


    public string QueryURL
    {
      get { return _queryURL; }
      set {
        if(_queryURL == value) return;
        _queryURL = value;
        OnPropertyChanged("QueryURL");
      }
    }
    private string _queryURL;


    #endregion

    #region -- Event ----------------

    public virtual void OnCurrentChanged()
    {
      if(CurrentChanged != null) CurrentChanged(this, EventArgs.Empty);
    }
    public event EventHandler CurrentChanged;
    //---
    public virtual bool OnCurrentChanging(bool isCancelable)
    {
      var arg = new CurrentChangingEventArgs(isCancelable);
      if(CurrentChanging != null) CurrentChanging(this, arg);
      return !(isCancelable && arg.Cancel);
    }
    public event CurrentChangingEventHandler CurrentChanging;

    public virtual void OnPageChanging(int newPageIndex_)
    {
      if(PageChanging != null) {
        PageChanging(this, new PageChangingEventArgs(newPageIndex_));
      }
    }
    public event EventHandler<PageChangingEventArgs> PageChanging;
    //-------
    public virtual void OnPageChanged()
    {
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
      if(PageChanged != null) PageChanged(this, EventArgs.Empty);
    }
    public event EventHandler<EventArgs> PageChanged;
    //--
    public event System.ComponentModel.AsyncCompletedEventHandler DataLoaded;
    public event System.ComponentModel.AsyncCompletedEventHandler SubmittedChanges;
    //--
    protected virtual void OnAddingNewEntity(Entity newEntity_)
    {
      if(this.AddingNewEntity == null) return;
      this.AddingNewEntity(this, new EventEntityAddingNewEntityArgs(newEntity_));
    }
    public event EventHandler<EventEntityAddingNewEntityArgs> AddingNewEntity;
    //++++++++++++++++++++++++++++++++


    #endregion

    #region INotifyPropertyChanged Members

    public void OnPropertyChanged(string propertyName_)
    {
      if(PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName_));
    }
    public event PropertyChangedEventHandler PropertyChanged;

    #endregion

    #region INotifyCollectionChanged Members

    public void OnCollectionChanged(NotifyCollectionChangedEventArgs arg_)
    {
      if(_suppressCollectionChangedCnt > 0) return;
      if(CollectionChanged != null) CollectionChanged(this, arg_);
    }
    public event NotifyCollectionChangedEventHandler CollectionChanged;

    #endregion

    #region Reakce na události Entity
    void entity_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
      switch(e.PropertyName) {
        case "IsChanged": EvalHasChanges(); break;
      }
    }

    void entity_EntityChanged(object sender, EventEntityChangedArgs e)
    {
      Entity ent = sender as Entity;
      int idx;
      switch(e.EntityAction) {
        case NotifyEntityAction.Edit:
        break;
        case NotifyEntityAction.AcceptChanges:
        if(ent.EntityState == EntityState.DeletedNew) {
          idx = _dataBuffer.IndexOf(sender);
          if(idx != -1) {
            _dataBuffer.Remove(sender);
            ent.Dispose();
            EvalHasChanges();
            EvalCanLoad();
            EvalCanChangePage();
            EvalItemCount();
          }
        }
          break;
        case NotifyEntityAction.Remove:
          idx = _dataView.IndexOf(sender);
          if(idx!=-1){
            _dataView.Remove(sender);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, sender, idx));
            OnPropertyChanged("Count");
          }
        break;
        //--------------------------------------------------
        case NotifyEntityAction.RejectChanges:
        if(ent.EntityState == EntityState.New || ent.EntityState == EntityState.DeletedNew) {
          // vyhodit, je uvolněn z paměti
          _dataBuffer.Remove(ent);
          idx = _dataView.IndexOf(ent);
          if(idx != -1) {
            _dataView.Remove(ent);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, ent, idx));
          }
        } else {
          // mělo by být UnModified
          idx = _dataView.IndexOf(sender);
          if(idx == -1) {
            _dataView.Add(sender);
            idx = _dataView.IndexOf(sender);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, sender, idx));
          } else {
            //OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, sender, idx));
          }
        }
        break;
        //---------------------------------------
      }
      EvalHasChanges();
    }

    void entity_EntityChanging(object sender, EventEntityChangingArgs e)
    {

    }

    #endregion

    #region ++ LoadData +++++++++++++++++++++++++++++++++

    internal class LoadHelper<TODataEntity> where TODataEntity : class
    {
      public LoadHelper(ODataSourceView pl_)
      {
        _pl = pl_;
      }
      private ODataSourceView _pl;
  
      public IQueryable<TODataEntity> OrderBy(IQueryable<TODataEntity> source)
      {
        var type = typeof(TODataEntity);
        var parameter = ex.Expression.Parameter(type, "p");

        //ex.Expression queryExpr = query.Expression;
        string methodAsc = "OrderBy";
        string methodDesc = "OrderByDescending";
        foreach(SortDescription item in _pl.SortDescriptions) {
          var property = type.GetProperty(item.PropertyName);
          if(property == null) throw new Exception("The orderby name " + item.PropertyName + " is invalid");
          var propertyAccess = ex.Expression.MakeMemberAccess(parameter, property);
          var orderByExp = ex.Expression.Lambda(propertyAccess, parameter);
          ex.MethodCallExpression resultExp = ex.Expression.Call(
            typeof(Queryable),
            item.Direction == ListSortDirection.Ascending ? methodAsc : methodDesc,
            new Type[] { type, property.PropertyType },
            source.Expression, ex.Expression.Quote(orderByExp)
            );
          methodAsc = "ThenBy";
          methodDesc = "ThenByDescending";
          source = source.Provider.CreateQuery<TODataEntity>(resultExp);
        }
        return source;
      }
  
      // sestaví request string
      public string GetURI(int startIndex_, int loadSize_)
      {
        // create query
        //DataServiceQuery<TODataEntity>
        IQueryable<TODataEntity> query = _pl._dataServiceContext.CreateQuery<TODataEntity>(_pl._ODataEntitySetQuery);
        //-- parametr
        var par = new ex.ParameterExpression[] { ex.Expression.Parameter(query.ElementType, "par") };
        //-- where --
        //-- order by-----
        if(_pl.Filter != null) {
          query = query.Provider.CreateQuery<TODataEntity>(
                ex.Expression.Call(
                    typeof(Queryable), "Where",
                    new Type[] { query.ElementType },
                    query.Expression, ex.Expression.Quote(_pl._filter)));

        }
        if(_pl.SortDescriptions != null && _pl.SortDescriptions.Count > 0) {
          query = OrderBy(query);
        }

        if(_pl.IncludeTotalCount) query = ((DataServiceQuery<TODataEntity>)query).IncludeTotalCount();

        if(startIndex_ > 0) query = query.Skip(startIndex_);
        query = query.Take(loadSize_);
        string res = query.ToString();
        if(!string.IsNullOrEmpty(_pl.ForeignKey)) {
          res = res.Replace(".svc/", ".svc/" + _pl.ForeignKey + "/");
        }
        return res;
      }
      //++ Fyzické čtení dat +++++++++++++++++++++++
      public void LoadData(int startIndex_, int loadSize_)
      {
        // -- Get Data ---------------------------
        string url = GetURI(startIndex_, loadSize_);
        _pl._loadingAsyncResult = _pl._dataServiceContext.BeginExecute<TODataEntity>(new Uri(url),
          (arg) => {
            if(_pl._loadingAsyncResult == arg) _pl._loadingAsyncResult = null;
            // ++ DataLoad Completed ++++++++++++++++++++++
            System.Data.Services.Client.QueryOperationResponse res=null;
           bool canceled = false;
           Exception exception = null;
           try {
            res = _pl._dataServiceContext.EndExecute<TODataEntity>(arg) as System.Data.Services.Client.QueryOperationResponse;
           } catch (InvalidOperationException ex_){
             canceled = true;
             exception = ex_;
           } catch(Exception ex_) {
             exception = ex_;
           }

            //++ Dispatcher +++++++++++++++++
            _pl.Dispatcher.BeginInvoke(() => {

              _pl.QueryURL = url;
              //todo:udělej Lock
              int oldTotalItemCount = _pl.TotalItemCount;
              _pl.DisposeData();
              int pageIndex = 0;
              if(exception==null) {
                foreach(var item in res) {
                  var entity = Activator.CreateInstance(_pl._EntityType) as Entity; // vytvořím entitu
                  entity.SetValuesFromODataEntity(item);
                  entity.EntityState = EntityState.Unmodified;
                  entity.ValidatePropertyValue = true; // aktivuji validaci property s ValidationAtrribute
                  _pl.AddInternal(entity);
                }

                //-- Data ----------
                _pl._startIndexBuffer = startIndex_;

                if(_pl.IncludeTotalCount) {
                  _pl.TotalItemCount = (int)res.TotalCount;
                } else if((_pl._dataBuffer.Count < loadSize_)) {
                  // nepřečetl vše. Jsem na konci
                  //TODO:Dodělat, pokud čtu TotalItemCount ze serveru
                  _pl.TotalItemCount = _pl._startIndexBuffer + _pl._dataBuffer.Count;
                } else _pl.TotalItemCount = -1; //řeším to přes _filterParHascode

                //ItemCount = _dataList.Count;
                pageIndex = _pl._deferredPageIndex;
                //var ip = _dataList.Par.ParametrGet("pageIndex");
                //if (ip != null) pageIndex = (int)ip.Value; // Toto jsem zadal

                // test - pokud se data změnila a jsem mimo
                if((_pl._dataBuffer.Count == 0) && (pageIndex > 0)) {
                  if(_pl._totalItemCount == -1) {
                    pageIndex = 0;
                  } else {
                    pageIndex = _pl._totalItemCount / _pl.PageSize;
                  }
                } 
              };
              _pl.IsLoadingCnt--;
              _pl.IsBusyCnt--;

              if(exception==null) {
                _pl.MoveToPage(pageIndex, _pl._deferredDirectionUp, _pl._deferredSetCurrenposition);
              }

              _pl.EvalCanLoad();
              _pl.EvalCanChangePage();
              _pl.EvalItemCount();
              if(oldTotalItemCount != _pl.TotalItemCount) _pl.OnPropertyChanged("TotalItemCount");

              if(_pl.DataLoaded != null) {
                _pl.DataLoaded(this, new AsyncCompletedEventArgs(exception, canceled, arg.AsyncState));
              }
            });
            //== Dispatcher ===================
            // == DataLoad Completed ===========================
          },
          null);
        _pl.IsLoadingCnt++;
        _pl.IsBusyCnt++;
        _pl.EvalCanLoad();
        _pl.EvalCanChangePage();
      }
    }

     private bool MoveToPage(int pageIndex_, bool directionUp_, enCurrentPosition setCurrenposition_)
    {
      if(IsDeferred) {
        _deferredPageIndex = pageIndex_;
        _deferredDirectionUp = directionUp_;
        _deferredSetCurrenposition = setCurrenposition_;
        return false;
      }

      if(!_allowLoadData) return false; //nechci číst data
      //-- kontrola indexu
      if((pageIndex_ < 0)
        || ((LastPageIndex != -1) && (pageIndex_ > LastPageIndex))) {
        // mimo rozsah
        return false;
      }
      //-- test na data v bufferu  ----------------------------------------
      if((_startIndexBuffer == -1) // chci číst nová data
        || (pageIndex_ * _pageSize < _startIndexBuffer)	// Začátek stránky je pod hranicí bufru
        || ((pageIndex_ * _pageSize >= (_startIndexBuffer + _dataBuffer.Count)) && (_dataBuffer.Count>0)) // Začátek stránky je  je nad hranicí buffru
        // není na konci a nemá kompletní stránku
        || ((_totalItemCount != _startIndexBuffer + _dataBuffer.Count) && ((pageIndex_ + 1) * _pageSize > (_startIndexBuffer + _dataBuffer.Count)))) 
			{
        //if(!CanLoad) return false;
        if(IsLoading) {
          CancelLoading();
        } else if(IsSubmitting) {
          CancelSubmitting();
        }

        //-- budu číst data do bufferu --------------
        //-- příkaz pro načtení dat,  MoveToPage se vyvolá asynchroně jako reakce na collectionChanget-Reset
        // co budu číst
        int si = (directionUp_) ? pageIndex_ * PageSize : ((pageIndex_ + 1) * PageSize) - LoadSize;
        if(si < 0) si = 0;

        _deferredPageIndex = pageIndex_; // až budeš mít data, pak zobraz tuto stránku
        _deferredDirectionUp = directionUp_;
        _deferredSetCurrenposition = setCurrenposition_;

        //----- Async Load Data ----------
        var loader = Activator.CreateInstance(typeof(LoadHelper<>).MakeGenericType(_ODataEntityType), this);
        loader.GetType().GetMethod("LoadData").Invoke(loader, new object[] { si, LoadSize });
        //--------------------------------


        return false;

      } else {
        //-- mam data v buffru -------------------
        //TODO: Toto by mělo být před DataLoad a tady testovat jestli bylo čteno
        IsPageChangingInternal++;
        OnPageChanging(pageIndex_); //todo: to dej do property

        // data jsou již načtena, vložím je do dataView
        PageIndex = pageIndex_;
        _startIndexView = _pageIndex * _pageSize - _startIndexBuffer;

        _dataView.Clear();
        int cnt = 0;
        for(int i = _startIndexView; i < _dataBuffer.Count; i++) {
          cnt++;
          if(cnt > _pageSize) break;
          _dataView.Add(_dataBuffer[i]);
        }

        IsPageChangingInternal--;
        OnPageChanged();
        if(!IsBusy && DataLoadCompleteInternal != null) DataLoadCompleteInternal();
        if(!IsBusy && DataLoadComplete != null) DataLoadComplete();

        // nastav viditelnost řádku
        int oldPos = CurrentPosition;
        if(setCurrenposition_ == enCurrentPosition.First) {
          CurrentPosition = 0;
        } else if(setCurrenposition_ == enCurrentPosition.Last) {
          CurrentPosition = Count - 1;
        } else {
          if(CurrentPosition >= Count) CurrentPosition = Count - 1;
          if(CurrentPosition==-1 && _dataView.Count>0) CurrentPosition = 0;
        }
        if(oldPos == CurrentPosition) { 
          // pozice stejná, změl se záznam
          OnCurrentChanged();
          OnPropertyChanged("CurrentItem");
          OnPropertyChanged("CurrentPrimaryKey");
        }


      }

      /*
      IsPageChangingInternal++;
      OnPageChanging(pageIndex_);
      try {
        L1:
        //-- kontrola, jestli ty data mám načtené
        if ((_dataListStartIndex == -1)
          || (pageIndex_ * _pageSize < _dataListStartIndex)	// pod buffrem
          || (pageIndex_ * _pageSize >= (_dataListStartIndex + _dataList.Count)) // nad buffrem
          || (((pageIndex_ + 1) * _pageSize >= (_dataListStartIndex + _dataList.Count)) && (_totalItemCount == -1))) // neznám kolik je pages
        {
          // Data nemám v bufru, čtu data ze serveru
          try {
            // co budu číst
            int si = (directionUp_) ? pageIndex_ * PageSize : ((pageIndex_ + 1) * PageSize) - _dataListLoadSize;
            if (si < 0) si = 0;

            IList newList = (IList)Slma.Gate.Primary.Exec(new ax.BoPar(_serviceModul, _serviceID) { startIndex = si, loadSize = _dataListLoadSize });
            // test - pokud se data změnila a jsem mimo
            if ((newList.Count == 0) && (pageIndex_ > 0)) {
              GetTotalCount();
              pageIndex_ = _totalItemCount / PageSize;
              goto L1;
            }
            _dataList = newList;
            _dataListStartIndex = si; ;

            if (ItemCount < (_dataListStartIndex + _dataList.Count)) {
              ItemCount = (_dataListStartIndex + _dataList.Count);
              if ((_dataList.Count < _dataListLoadSize)) {
                // nepřečetl vše. Jsem na konci
                TotalItemCount = ItemCount;
              }
            }

          } catch (Exception ex_) {
            // TODO:exception handler
            return false;
          }
        }
        // data jsou již načtena
        PageIndex = pageIndex_;

      }finally{
        IsPageChangingInternal--;
        OnPageChanged();
      }
       */
      //TODO: Move to Page data changed
      return true;
    }

    #endregion

     public Entity CreateEntity()
     {
       // Entitu nepřipojuji do seznamu
       var entity = Activator.CreateInstance(_EntityType) as Entity; // vytvořím entitu
       entity.EntityState = EntityState.New;
       return entity;
     }

     private IAsyncResult _submitAsyncResult;
     public IAsyncResult SubmitChanges(Action<AsyncCompletedEventArgs> CallBack_)
     {
       if(IsBusy) throw new Exception("IsBusy");

       // uložím změny do serviceContextu
       foreach(Entity ent in _dataBuffer) {
         switch(ent.EntityState) {
           case EntityState.Modified:
           _dataServiceContext.UpdateObject(ent.GetODataEntity());
           break;
           case EntityState.New:
           _dataServiceContext.AddObject(_ODataEntitySetName, ent.CreateODataEntity(_ODataEntityType));
           break;
           case EntityState.Deleted:
            _dataServiceContext.DeleteObject(ent.GetODataEntity());
           break;
         }
       }
       // volám service context
       _submitAsyncResult = _dataServiceContext.BeginSaveChanges(SaveChangesOptions.Batch,(arg)=>{

           if(_submitAsyncResult == arg) _submitAsyncResult = null;
           bool canceled = false;
           string error = null;
           Exception exception=null;
           try {
             DataServiceResponse res = _dataServiceContext.EndSaveChanges(arg);
           } catch(InvalidOperationException ex_) {
             canceled = true;
             exception = ex_;
           } catch(Exception ex_) {
             error = ex_.Message;
             exception = ex_;
           }
           //++ Dispatcher +++++++++++++++++
           //Dispatcher.BeginInvoke(() => {
             if(!canceled || !string.IsNullOrEmpty(error)) {
               // nastavuji entity do stavu Unchanged
               for(int i = _dataBuffer.Count - 1; i >= 0; i--) {
                 Entity ent = _dataBuffer[i] as Entity;
                 switch(ent.EntityState) {
                   case EntityState.Undefined:
                   case EntityState.Modified:
                   case EntityState.New:
                   ent.EntityState = EntityState.Unmodified;
                   break;
                   case EntityState.Deleted:
                   case EntityState.DeletedNew:
                   _dataBuffer.Remove(ent);
                   ent.Dispose();
                   break;
                   case EntityState.Disposed:
                   _dataBuffer.Remove(ent);
                   break;
                 }
               }
             }
             if(canceled) Clear();

             if(CallBack_ != null) {
               CallBack_(new AsyncCompletedEventArgs(exception,canceled,arg.AsyncState));
             }

             IsSubmittingCnt--;// IsSubmitting=false;
             IsBusyCnt--;
             
             EvalHasChanges();
             EvalCanLoad();
             EvalCanChangePage();

             if(SubmittedChanges != null) {
               SubmittedChanges(this, new AsyncCompletedEventArgs(exception, canceled, arg.AsyncState));
             }
           //}); //Dispatcher

       },null);
       IsSubmittingCnt++; // IsSubmitting=true;
       IsBusyCnt++;
       return _submitAsyncResult;
     }
 
    private IAsyncResult _loadingAsyncResult;
    public void CancelLoading()
    {
      if(_loadingAsyncResult != null) {
        _dataServiceContext.CancelRequest(_loadingAsyncResult);
      }
    }
 
     public void CancelSubmitting()
     {
       if(_submitAsyncResult != null) {
         _dataServiceContext.CancelRequest(_submitAsyncResult);
       }
     }

     public void RejectChanges()
     {
       _suppressCollectionChangedCnt++;
       for(int i = _dataBuffer.Count-1; i >=0 ; i--) {
         Entity ent = _dataBuffer[i] as Entity;
         switch(ent.EntityState) {
           case EntityState.Undefined:
           case EntityState.Unmodified:
           break;
           case EntityState.Disposed:
           case EntityState.New:
           case EntityState.DeletedNew:
           case EntityState.Deleted:
           case EntityState.Modified:
           ent.RejectChanges(true);// neposílej eventy, udělám to na konci pro celý grid 
           break;
         }
       }  
       _suppressCollectionChangedCnt--;
       // naplním data
       _dataView.Clear();
       OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
       int cnt = 0;
       for(int i = _startIndexView; i < _dataBuffer.Count; i++) {
         cnt++;
         if(cnt > _pageSize) break;
         _dataView.Add(_dataBuffer[i]);
       }
       if(CurrentPosition >= Count) CurrentPosition = Count - 1;
       //--
       EvalHasChanges();
       EvalCanLoad();
       EvalCanChangePage();
       // refresh obrazovky
       OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
     }

    #region Internal Methods

    private void ValidEntityType(object Entity_)
    {
      if(Entity_.GetType() != _EntityType) throw new Exception("Type of object is not valid.");
    }
    //--------
    //-- kontrola povolených operací editable --
    protected internal void EnsureEditable(EntityListOperations operation)
    {
      if((this._supportedOperations & operation) == EntityListOperations.None) {
        throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Resource.EntityList_UnsupportedOperation, new object[] { _EntityType, operation }));
      }
    }
    protected bool IsSupported(EntityListOperations operation)
    {
      return ((this._supportedOperations & operation) != EntityListOperations.None);
    }

    #endregion

    #region Public Methods

    #endregion

    #region -- InnerFlags -----------------------------------

    [Flags]
    private enum InnerFlags : int
    {
      OperationAdd = 0x01,
      OperationEdit = 0x02,
      OperationRemove = 0x04,
      OperationAll = 0x07,


      /// <summary>
      /// Whether we should process the collection changed event
      /// </summary>
      ShouldProcessCollectionChanged = 0x0100,

      /// <summary>
      /// Whether the source collection is already sorted according to the SortDescriptions collection
      /// </summary>
      //IsDataSorted = 0x0200,

      /// <summary>
      /// Whether we need to refresh
      /// </summary>
      //NeedsFirstPage = 0x0400,

      /// <summary>
      /// Whether we need to refresh
      /// </summary>
      //NeedsRefresh = 0x0800,
    }
    //----------
    private InnerFlags _flags = InnerFlags.ShouldProcessCollectionChanged;
    //----------
    private void SetFlag(InnerFlags flags, bool value)
    {
      if(value) {
        this._flags = this._flags | flags;
      } else {
        this._flags = this._flags & ~flags;
      }
    }
    //----------
    private bool CheckFlag(InnerFlags flags)
    {
      return (this._flags & flags) != 0;
    }

    #endregion

    #region -- DeferHelper ----------------------------------------------

    /// <summary>
    /// Used to keep track of Defer calls on the PagedCollectionView, which
    /// will prevent the user from calling Refresh() on the view. In order
    /// to allow refreshes again, the user will have to call IDisposable.Dispose,
    /// to end the Defer operation.
    /// </summary>
    private class DeferHelper : IDisposable
    {
      /// <summary>
      /// Private reference to the CollectionView that created this DeferHelper
      /// </summary>
      private ODataSourceView _list;

      /// <summary>
      /// Initializes a new instance of the DeferHelper class
      /// </summary>
      /// <param name="collectionView">CollectionView that created this DeferHelper</param>
      public DeferHelper(ODataSourceView list_)
      {
        this._list = list_;
        list_.StartDefer();
      }

      /// <summary>
      /// Cleanup method called when done using this class
      /// </summary>
      public void Dispose()
      {
        if(_list != null) _list.EndDefer();
        _list = null;
        GC.SuppressFinalize(this);
      }
    }

    //-------------------------
    private void StartDefer()
    {
      _deferredLevel++;
    }
    //-------------------------
    private void EndDefer()
    {
      _deferredLevel--;
      if(_deferredLevel == 0) {
        if(_deferredNeedRefresh) {
          _startIndexBuffer=-1; // indikace, že chci nová data
          _deferredNeedRefresh = false;
        }
        MoveToPage(_deferredPageIndex,_deferredDirectionUp,_deferredSetCurrenposition);// vyvolá načtení dat
      }
    }

    #endregion

    #region IEnumerator
		public IEnumerator GetEnumerator()
    {
      return _dataView.GetEnumerator();
    }
 
	  #endregion  

    #region IList

    private void AddInternal(Entity entity)
    {
      if(entity.EntityState == EntityState.Disposed)
        throw new Exception(string.Format(CultureInfo.CurrentCulture, Resource.EntityIsDisposed, null));

      if(entity.EntityState == EntityState.Undefined) entity.EntityState = EntityState.New;

      // navážu eventy
      entity.EntityChanging += new EventHandler<EventEntityChangingArgs>(entity_EntityChanging);
      entity.EntityChanged += new EventHandler<EventEntityChangedArgs>(entity_EntityChanged);
      entity.PropertyChanged += new PropertyChangedEventHandler(entity_PropertyChanged);
      // přidám entitu do hlavního seznamu
      _dataBuffer.Add(entity);
    }

		public int Add(object value)
    {
      this.EnsureEditable(EntityListOperations.Add);
      ValidEntityType(value);

      int idx = -1;

      var entity = value as Entity;
      lock(SyncRoot) {
        AddInternal(entity);
        // přidám entitu do view
        if(entity.EntityState != EntityState.Deleted && entity.EntityState != EntityState.Deleted) {
          _dataView.Add(entity);
          idx = _dataView.IndexOf(entity);
          OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entity, idx));
          OnPropertyChanged("Count");
          EvalHasChanges();
          EvalCanLoad();
          EvalCanChangePage();
          CurrentPosition = IndexOf(value);
        }
      }
      return idx;
    }

    private void DisposeData()
    {
      foreach(Entity item in _dataBuffer) {
        item.Dispose();
      }
      _dataView.Clear();
      _dataBuffer.Clear();
      _startIndexBuffer = -1;
      _totalItemCount = -1;
    }

    public void Clear()
    {
      lock(SyncRoot) {
        DisposeData();

        _startIndexView = 0;
        TotalItemCount = -1;
        //todo:ItemCount = 0;
        PageIndex = 0;
        CurrentPosition = -1;
        HasChanges = false;

        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
      }
    }

    public bool Contains(object value)
    {
      return _dataView.Contains(value);
    }

    public int IndexOf(object value)
    {
      return _dataView.IndexOf(value);  
    }

    public void Insert(int index, object value)
    {
      this.EnsureEditable(EntityListOperations.Add);
      ValidEntityType(value);

      var entity = value as Entity;
      if(entity.EntityState == EntityState.Disposed)
        throw new Exception(string.Format(CultureInfo.CurrentCulture, Resource.EntityIsDisposed, null));
      if(entity.EntityState == EntityState.Deleted || entity.EntityState == EntityState.Deleted)
        throw new Exception(string.Format(CultureInfo.CurrentCulture, Resource.EntityIsDeleted, null));


      if(entity.EntityState == EntityState.Undefined) entity.EntityState = EntityState.New;

      lock(SyncRoot) {
        // navážu eventy
        entity.EntityChanging += new EventHandler<EventEntityChangingArgs>(entity_EntityChanging);
        entity.EntityChanged += new EventHandler<EventEntityChangedArgs>(entity_EntityChanged);
        entity.PropertyChanged += new PropertyChangedEventHandler(entity_PropertyChanged);
        // přidám entitu do hlavního seznamu
        _dataBuffer.Insert(index,entity);
        _dataView.Add(entity);
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entity, null, index));
        OnPropertyChanged("Count");
        //todo: if(entity.IsChanged) HasChanges = true;
        //todo:nastav current position
        //todo:if(hadChanges) OnPropertyChanged("HasChanges");
        //todo:if(hadData) OnPropertyChanged("IsChanged");
      }
    }

    public bool IsFixedSize
    {
      get { return false; }
    }

    public bool IsReadOnly
    {
      get { return (this._supportedOperations == EntityListOperations.None); }
      set {
        if((this._supportedOperations == EntityListOperations.None) == value) return;
        this._supportedOperations = (value)?EntityListOperations.All:EntityListOperations.None;  
      }
    }

    public void Remove(object value)
    {
      this.EnsureEditable(EntityListOperations.Remove);
      ((Entity)value).Remove(); // akci provádím v reakci na entity_changed
    }

    public void RemoveAt(int index)
    {
      this.EnsureEditable(EntityListOperations.Remove);
      ((Entity)_dataView[index]).Remove();
    }

    public object this[int index]
    {
      get { return _dataView[index]; }
      set { throw new NotImplementedException(); }
    }

    public void CopyTo(Array array, int index)
    {
      throw new NotImplementedException();
    }

    public int Count
    {
      get { return _dataView.Count; }
    }

    public bool IsSynchronized
    {
      get { return true; }
    }

    public object SyncRoot
    {
      get { return _dataView; }
    }
 
	#endregion  
  
    #region ICollectionView

    private bool _canFilter = false;
    public bool CanFilter
    {
      get { return _canFilter; }
      private set
      {
        if(_canFilter != value) return;
        _canFilter = value;
        OnPropertyChanged("CanFilter");
      }
    }
    //---
    public bool CanGroup
    {
      get { return false; }
    }
    //---
    private bool _canSort = true;
    public bool CanSort
    {
      get { return _canSort; }
      private set
      {
        if(_canSort != value) return;
        _canSort = value;
        OnPropertyChanged("CanSort");
      }
    }
    //---
    private CultureInfo _culture = System.Threading.Thread.CurrentThread.CurrentCulture;
    public CultureInfo Culture
    {
      get { return _culture; }
      set
      {
        if(_culture != value) return;
        _culture = value;
        OnPropertyChanged("Culture");
      }
    }

    //---
    public string ForeignKey
    {
      get { return _foreignKey; }
      set {
        if(_foreignKey == value) return;
        _foreignKey = value;
        OnPropertyChanged("ForeignKey");
        if(string.IsNullOrEmpty(_foreignKey)) {
          Clear();
        } else {
          Clear();
          MoveToFirstPage();
        }
      }
    }
    private string _foreignKey;
    //---
    public string CurrentPrimaryKey
    {
      get {
        string res = null;
        Entity ent = CurrentItem as Entity;
        if(ent != null) {
          System.Text.StringBuilder sb = new StringBuilder(_ODataEntitySetQuery);
          sb.Append("(");
          foreach(string itemKey in _primaryKeyNames) {
            //todo:dodělat int key a více klíčů
            var keyVal = ent.GetValue(itemKey);
            if(keyVal is Guid) {
              sb.Append("guid'");
              sb.Append(((Guid)keyVal).ToString("D"));
              sb.Append("'");
            } else if(keyVal is string) {
              sb.Append("'");
              sb.Append(keyVal.ToString());
              sb.Append("'");
            } else sb.Append(keyVal.ToString());
          }
          sb.Append(")");
          res = sb.ToString();
        }
        return res; 
      }
    }
    //---
    public object CurrentItem
    {
      get
      {
        if(IsCurrentBeforeFirst || IsCurrentAfterLast) return null;
        return this[_currentPosition];
      }
    }
    //---
    private int _currentPosition;
    public int CurrentPosition
    {
      get { return _currentPosition; }
      set
      {
        if(value < -1) value = -1;
        if(value >= this.Count) value = this.Count - 1;
        if(_currentPosition == value) return;
        if(!OnCurrentChanging(true)) return; // může být cancel
        _currentPosition = value;
        OnCurrentChanged();
        OnPropertyChanged("CurrentPosition");
        OnPropertyChanged("CurrentItem");
        OnPropertyChanged("CurrentPrimaryKey");
      }
    }

    public IDisposable DeferRefresh()
    {
      return new DeferHelper(this);
    }
    //-- Nepoužívat, toto filtruje již načtená data. Tady potřebuji expression
    Predicate<object> ICollectionView.Filter
    {
      get
      {
        throw new NotImplementedException();
      }
      set
      {
        throw new NotImplementedException();
      }
    }
    public ex.Expression Filter
    {
      get { return _filter; }
      set
      {
        if(_filter == value) return;
        _filter = value;
        if(IsDeferred) {
          _deferredNeedRefresh = true;
        } else {
          _startIndexBuffer = -1; // indikace, že chci nová data
          MoveToFirstPage();
        }
      }
    }
    private ex.Expression _filter;

    //--
    public ObservableCollection<GroupDescription> GroupDescriptions
    {
      get { return null; }
    }
    //---
    public ReadOnlyObservableCollection<object> Groups
    {
      get { return null; }
    }
    //--
    public bool IsCurrentAfterLast
    {
      get { return (this.CurrentPosition >= this.Count); }
    }

    public bool IsCurrentBeforeFirst
    {
      get { return (this.CurrentPosition < 0); }
    }

    public bool IsEmpty
    {
      get { return this.Count == 0; }
    }

    public bool MoveCurrentTo(object item)
    {
      int idx = _dataView.IndexOf(item);
      if(idx == -1) return false; // nenašel
      CurrentPosition = idx;
      return CurrentPosition == idx;
    }

    public bool MoveCurrentToFirst()
    {
      if((Count == 0)) return false;// nejsou data
      CurrentPosition = 0;
      return CurrentPosition == 0;
    }

    public bool MoveCurrentToLast()
    {
      int idx = Count - 1;
      if((idx < 0)) return false;// nejsou data
      CurrentPosition = idx;
      return CurrentPosition == idx;
    }

    public bool MoveCurrentToNext()
    {
      int idx = CurrentPosition+1;
      if(idx >= Count) return false;
      CurrentPosition = idx;
      return CurrentPosition == idx;
    }

    public bool MoveCurrentToPosition(int position)
    {
      int idx = position;
      if((idx < 0) || (idx >= Count)) return false;
      CurrentPosition = idx;
      return CurrentPosition == idx;
    }

    public bool MoveCurrentToPrevious()
    {
      int idx = CurrentPosition-1;
      if(idx < 0) return false;
      CurrentPosition = idx;
      return CurrentPosition == idx;
    }

    public void Refresh() // Znova načte data
    {
      if(IsBusy) throw new Exception("");
      _startIndexBuffer = -1; // smaž data
      //_hasChanges = false; // jinak je CanLoad=false
      //_canLoad = true; // vynucený load
      MoveToPage(PageIndex, true, enCurrentPosition.None);
    }

    private SortDescriptionCollection _sortDescriptionCollection;
    public SortDescriptionCollection SortDescriptions
    {
      get
      {
        if(_sortDescriptionCollection == null) {
          _sortDescriptionCollection = new SortDescriptionCollection();
          ((INotifyCollectionChanged)_sortDescriptionCollection).CollectionChanged += new NotifyCollectionChangedEventHandler(SortDescriptionChanged);
        }
        return _sortDescriptionCollection;
      }
    }
    //----------------
    void SortDescriptionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
      if(IsDeferred) {
        _deferredNeedRefresh = true;
        _deferredPageIndex = 0;
      } else {
        _startIndexBuffer = -1; // indikace, že chci nová data
        MoveToFirstPage();
      }
    }
    //----------------
    public IEnumerable SourceCollection
    {
      get { return this; }
    }

    #endregion

    #region IPagedCollectionView

    #region CanChangePage
    public bool CanChangePage
    {
      get { return _canChangePage;  }
      private set { 
        if(_canChangePage == value)return;
        _canChangePage = value;
        OnPropertyChanged("CanChangePage");
      } 
    }
    private bool _canChangePage;
    private void EvalCanChangePage() { CanChangePage = !IsPageChanging && CanLoad; }

    #endregion
    #region ItemCount
    //--------------
    // kolik položek z tohoto View je mi známo
    public int ItemCount
    {
      get {  return _itemCount;  }
      private set {
        if(_itemCount == value) return;
        _itemCount = value;
        OnPropertyChanged("ItemCount");
      }
    }
    private int _itemCount;
    private void EvalItemCount(){
      if(_totalItemCount == -1) ItemCount = (_startIndexBuffer == -1) ? 0 : _startIndexBuffer + _dataBuffer.Count;
      else ItemCount = _totalItemCount;
    }

    #endregion
    //--------------
    public bool MoveToFirstPage()
    {
      return MoveToPage(0, true, enCurrentPosition.First);
    }
    //--------------
    public bool MoveToLastPage()
    {
      //if(_totalItemCount == -1) GetTotalCount();
      return MoveToPage(LastPageIndex, false, enCurrentPosition.Last);
    }
    //--------
    public bool MoveToNextPage(enCurrentPosition setCurrentPosition_)
    {
      return MoveToPage(_pageIndex + 1, true, setCurrentPosition_);
    }
    //--------------
    public bool MoveToNextPage()
    {
      return MoveToPage(_pageIndex + 1, true, enCurrentPosition.None);
    }
    //--------------
    public bool MoveToPage(int pageIndex)
    {
      return MoveToPage(pageIndex, true, enCurrentPosition.None); 
    }
    //------------
    public bool MoveToPreviousPage(enCurrentPosition setCurrentPosition_)
    {
      return MoveToPage(_pageIndex - 1, false, setCurrentPosition_);
    }
    //--------------
    public bool MoveToPreviousPage()
    {
      return MoveToPage(_pageIndex - 1, false, enCurrentPosition.None);
    }
    //--------------
    public int TotalItemCount
    {
      get { return _totalItemCount; }
      private set
      {
        if(_totalItemCount == value) return;
        _totalItemCount = value;
        OnPropertyChanged("TotalItemCount");
      }
    }
    private int _totalItemCount = -1;
    //--
    public int PageSize 
    {
      get { return _pageSize; }
      set
      {
        if(_pageSize == value) return;
        _pageSize = value;
        OnPropertyChanged("PageSize");
      }
    }
    private int _pageSize;
    //--------------
    public int PageIndex
    { 
      get { return _pageIndex; }
      private set {
        if(_pageIndex == value) return;
        _pageIndex = value;
        OnPropertyChanged("PageIndex");
      }
    }
    private int _pageIndex;
    //--------------

	#endregion  
  
  
    #region IEditableCollectionView

    private Entity EditingEntity
    {
      get { return _editingEntity; }
      set {
        if(_editingEntity == value) return;
        _editingEntity = value;
        OnPropertyChanged("IsEditingItem");
        OnPropertyChanged("CanCancelEdit");
        OnPropertyChanged("CurrentEditItem");
      }
    }
    private Entity _editingEntity;
    //---
    public object CurrentEditItem
    {
      get { return EditingEntity; }
    }
    //---
    public void EditItem(object item)
    {
      ValidEntityType(item);
      if(EditingEntity != null) { CancelEdit(); }
      EditingEntity = item as Entity;
      EditingEntity.BeginEdit();
    }
    //---
    public bool CanCancelEdit
    {
      get { return EditingEntity != null; }
    }
    //--
    public void CancelEdit()
    {
      if(EditingEntity != null) {
        EditingEntity.CancelEdit();
        EditingEntity = null;
      }
    }
    //--
    public void CommitEdit()
    {
      if(EditingEntity != null) EditingEntity.EndEdit();
      EditingEntity = null;
    }
    //--
    public bool IsEditingItem
    {
      get { return _editingEntity != null; }
    }
    //------------------------
    private Entity AddingEntity
    {
      get { return _addingEntity; }
      set
      {
        if(_addingEntity == value) return;
        _addingEntity = value;
        OnPropertyChanged("CanAddNew");
        OnPropertyChanged("IsAddingNew");
        OnPropertyChanged("CurrentAddItem");
      }
    }
    private Entity _addingEntity;
    //--
    public object AddNew()
    {
      if(AddingEntity != null) { CancelNew(); }
      var entity = CreateEntity();
      entity.ValidatePropertyValue = true;
      AddingEntity = entity;
      OnAddingNewEntity(entity);
      NewItemPlaceholderPosition = System.ComponentModel.NewItemPlaceholderPosition.None;
      Add(_addingEntity);
      return entity;
    }
    //----
    public bool CanAddNew
    {
      get { return IsSupported(EntityListOperations.Add) && _addingEntity==null; }
    }
    //---
    public void CommitNew()
    {
      if(_addingEntity == null && _addingEntity.EntityState!=EntityState.New) throw new ArgumentException();
      AddingEntity = null;
    }
    //---
    public void CancelNew()
    {
      if(_addingEntity == null && _addingEntity.EntityState != EntityState.New) throw new ArgumentException();
      _addingEntity.Remove();
      _addingEntity.AcceptChanges();
      AddingEntity = null;
    }
    //--
    public object CurrentAddItem
    {
      get { return _addingEntity; }
    }
    //-----------
    public bool IsAddingNew
    {
      get { return _addingEntity != null; }
    }
    //---------
    public NewItemPlaceholderPosition NewItemPlaceholderPosition { get; set; }
    //--
    public bool CanRemove
    {
      get { return IsSupported( EntityListOperations.Remove); }
    }




    #endregion
  }
}
